CSDV3017  ·  DEVOPS  ·  SCHOOL OF COMPUTER SCIENCE, UPES

Hands-On: Setting Up CI/CD & Infrastructure Automation

Lecture 14 — rolling up our sleeves to write actual YAML workflows for GitHub Actions, creating Dockerfiles, and provisioning infrastructure using Terraform and Ansible playbooks.

InstructorDr. Mohsin Furkh Dar
SessionWeek 5 · Tue, 07 Jul 2026
Time12:00 – 13:00
UnitUnit V
I
II
III
IV
V
VI
VII
PRACTICAL SESSION

Today's hands-on agenda

Part 01

Continuous Integration

  • Building a Node.js app
  • Writing a GitHub Actions YAML workflow
  • Running tests on every Push
Part 02

Containerization

  • Writing a Dockerfile
  • Building the Docker image
  • Running it locally
Part 03

Infrastructure Automation

  • Provisioning an AWS EC2 instance with Terraform
  • Configuring the instance with Ansible

Note: In this session, we will look at the exact code and command-line steps required to build out a complete pipeline from code to infrastructure.

STEP 0

The Target Application

The Codebase

A simple Node.js API

We have a basic Express application with a few endpoints. It uses npm start to run and npm test to run a suite of Jest tests.

// package.json scripts
{
  "name": "devops-demo-api",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}
Git Push
CI Pipeline (Test)
Docker Build
Terraform (Infra)
Ansible (Config)
PART 01 : CI PIPELINE

Setting up GitHub Actions

File location: .github/workflows/ci.yml

name: Node.js CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18.x'

    - name: Install dependencies
      run: npm ci

    - name: Run Tests
      run: npm test
CI PIPELINE BREAKDOWN

What just happened?

The Trigger

on: push & pull_request

This tells GitHub to run the pipeline automatically whenever someone pushes code to the main branch, or opens a Pull Request targeting main. This is the heart of Continuous Integration.

The Runner

runs-on: ubuntu-latest

GitHub automatically provisions a clean, temporary Ubuntu virtual machine for us. We don't have to manage any servers to run our CI.

The Actions

uses: ...

These are pre-built, community steps. checkout@v3 clones our Git repo into the runner. setup-node@v3 installs the correct version of Node.js.

The Commands

run: npm test

These are the actual bash commands executed on the runner. npm ci strictly installs dependencies from the lockfile, and npm test executes Jest.

PART 02 : CONTAINERIZATION

Writing the Dockerfile

File location: Dockerfile (in the root of the project)

# 1. Use the official Node.js image as our base
FROM node:18-alpine

# 2. Set the working directory inside the container
WORKDIR /app

# 3. Copy package.json and install dependencies
COPY package*.json ./
RUN npm ci --only=production

# 4. Copy the rest of the application code
COPY . .

# 5. Expose the port the app runs on
EXPOSE 3000

# 6. Define the command to start the app
CMD ["npm", "start"]
CONTAINER WORKFLOW

Building the image

Command Line

Building and running the Docker container

Once the Dockerfile is written, we use the Docker CLI to build the immutable image and run it.

# Build the image and tag it (-t) as 'devops-api:v1'
$ docker build -t devops-api:v1 .

# Output:
# => [internal] load build definition from Dockerfile
# => => transferring context: 2.11kB
# => [1/4] FROM docker.io/library/node:18-alpine
# ...
# => exporting to image

# Run the container in detached mode (-d) and map port 8080 to 3000 (-p)
$ docker run -d -p 8080:3000 devops-api:v1

# Verify it's running
$ docker ps
PART 03 : INFRA AS CODE

Provisioning with Terraform

File location: main.tf (Now we need a server to run our container on).

provider "aws" {
  region = "us-east-1"
}

resource "aws_security_group" "web_sg" {
  name = "allow_web_traffic"
  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "prod_server" {
  ami = "ami-0c55b159cbfafe1f0" # Ubuntu
  instance_type = "t2.micro"
  vpc_security_group_ids = [aws_security_group.web_sg.id]
  key_name = "deploy_key"
}
TERRAFORM WORKFLOW

Executing Terraform

Initialize

terraform init

Downloads the AWS provider plugins needed to execute the code.

Plan

terraform plan

Generates an execution plan. It says: "I will create 1 Security Group and 1 EC2 instance."

Apply

terraform apply

Actually makes the API calls to AWS to create the resources. Generates a terraform.tfstate file.

$ terraform apply

# Plan: 2 to add, 0 to change, 0 to destroy.
# Do you want to perform these actions?
# Terraform will perform the actions described above.
# Only 'yes' will be accepted to approve.

Enter a value: yes

# aws_instance.prod_server: Creating...
# Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
PART 04 : CONFIG MANAGEMENT

Configuring the server with Ansible

Now that Terraform created the raw Ubuntu VM, we use Ansible to install Docker and run our application container.

# deploy.yml (Ansible Playbook)

- name: Setup Production Server
  hosts: prod_server
  become: yes

  tasks:
    - name: Install Docker
      apt:
        name: docker.io
        state: present
        update_cache: yes

    - name: Ensure Docker is running
      service:
        name: docker
        state: started

    - name: Run our application container
      community.docker.docker_container:
        name: api_server
        image: myregistry/devops-api:v1
        state: started
        ports:
          - "80:3000"
THE FULL PIPELINE

The Complete DevOps Architecture

The Big Picture

How these files interact

We've just built a fully automated pipeline. If a developer changes code, here is what happens automatically:

  • Step 1: Developer pushes code to GitHub.
  • Step 2: GitHub Actions reads .github/workflows/ci.yml and runs the Jest tests.
  • Step 3: If tests pass, GitHub Actions reads the Dockerfile, builds the image, and pushes it to a registry.
  • Step 4: GitHub Actions triggers Ansible (deploy.yml), which connects to the AWS server created by Terraform (main.tf).
  • Step 5: Ansible pulls the new Docker image and restarts the container. The new version is live.
WRAP-UP

Summary & what's next

Lecture 14 · Key Takeaways

What you should remember

  • GitHub Actions (YAML): Uses on: push triggers, runs on virtual machines, executes shell commands.
  • Dockerfile: The recipe for an image. FROM, WORKDIR, COPY, RUN, CMD.
  • Terraform (HCL): Declarative infrastructure. provider, resource. Workflow is Init → Plan → Apply.
  • Ansible (YAML): Configuration management. Uses playbooks and tasks to ensure a server is in the correct state (e.g., Docker installed and running).
Next Lecture · Lecture 15

Tue, 07 Jul 2026 · 14:00–15:00 · Unit VI

Introduction to Testing; Verification & Validation; Types of Testing (White-box, Manual, Automation).

Unit V Complete ✓

You've finished Unit V!

We've successfully moved from DevOps theory into actual implementation and coding. Unit VI will focus on the Testing phase of the lifecycle.

CSDV3017 · DEVOPS
SHEET 01/12